#!/usr/bin/env lua
-- 
-- $Id: ipv6util.lua 7557b5d4d82b 2012/12/02 09:07:16 pgurumur $
-- Copyright (c) 2009 Prabhu Gurumurthy <pgurumur@gmail.com>
-- 
-- Permission to use, copy, modify, and distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
-- 
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-- 
-- What does this script do
--    Generate IPv6 prefix or node address and inserts the prefix to the
--    database. This script is loosely based on RFC4193, infact it is kind
--    of more robust that the RFC4193, but does not strictly adhere to the
--    way specified in the RFC.
--
-- Usage examples
-- ipv6util.lua --length 64
--       Will generate 64 bit address
--
-- ipv6util.lua --prefix --length 64
--       Will generate 64 bit Prefix
--
-- ipv6util.lua --prefix --length 48 --db --cust "Lua Wan"
--       Will do the following
--       1. Generate 48 bit prefix
--       2. Insert the following SQL statement into db specified in
--          /opt/local/etc/db.cfg
--          insert into IPv6_Prefix(NULL, "48 bit Prefix", "Lua Wan");
--
-- ipv6util.lua -n --prefix --length 48 --db --cust "Lua Wan"
--       Will do the following
--       1. Generate 48 bit prefix
--       2. Print the following SQL statement and NOT into db
--          insert into IPv6_Prefix(NULL, "48 bit Prefix", "Lua Wan");
--
-- Requires the prefix to be in multiple of 4, more granular is not implemented
-- right now, may be in future revisions

-- local imports
require "lintf"   -- Get the MAC addresses from this system
require "digest"  -- Get the digest, support whatever OpenSSL supports
require "luasql.mysql"
require "syslog"

-- local variables
-- always integers, followed by everything else, easy on somebody reading
-- it
local argc = 0
local mod = 4
local dry = false
local debug = false
local program = nil
local main = nil

-- For major revision, bump up the version number, do not bump it up if it 
-- is minor revisions and comment changes
local version = 1.3

-- What the maximum IPv6 length
local ipv6MaxLen = 128

-- So that we can use printf(3)
printf = function(str, ...)
   io.write(str:format(...))
end

-- similar err(3)
err = function(str, ...)
   printf("%s: ", program)
   io.write(str:format(...))
   printf("\n")
   os.exit(1)
end

verbose = function(str, ...)
   if debug then
      printf(str, ...)
      printf("\n")
   end
end

-- Used to get argc
function getn(tbl)
   local index = 0
   for keys in pairs(tbl) do
      if keys >= 0 then
         index = index + 1
      end
   end

   return index
end

-- Open syslog
function openLog()
   syslog.openlog(program, syslog.LOG_PERROR + syslog.LOG_ODELAY, "LOG_USER")
end

-- close syslog
function closelog()
   syslog.closelog()
end

-- syslog
function logging(facility, str, ...)
   if not facility then
      facility = "LOG_WARNING"
   end

   local mesg = str:format(...)
   syslog.syslog(facility, mesg)
end


-- Returns table size
function getSize(tbl)
   local index = 0
   for keys in pairs(tbl) do
      index = index + 1
   end

   return index
end

-- split function just like perl split
-- given /usr/bin/foo, with pat being /, it will parse the string to
-- table elements containing { usr, bin, foo }
function split(str, pat) 
   local retval = {}  -- NOTE: use {n = 0} in Lua-5.0
   local fpat = "(.-)" .. pat 
   local last_end = 1 
   local string, ix, cap = str:find(fpat, 1) 
   while string do 
      if string ~= 1 or cap ~= "" then 
         table.insert(retval, cap) 
      end 
      
      last_end = ix + 1 
      string, ix, cap = str:find(fpat, last_end)
   end 
   if last_end <= #str then 
      cap = str:sub(last_end) 
      table.insert(retval, cap) 
   end 
   return retval 
end

-- Given a table, get the value from its highest index
function getargv(tbl)
   local index = 0
   for key, value in pairs(tbl) do
      index = index + 1
   end

   return tbl[index]
end

-- print usage of this code and exit
function usage()
   printf("%s version: %0.01f\n", program, version)
   printf("%s: options\n", program)
   printf("options: \n")
   printf("\t-p | --prefix [ Generate prefix, not just IPv6 addr ]\n")
   printf("\t-l | --length [ Length of IPv6 address/prefix (-p | --prefix) ]\n")
   printf("\t-d | --db     [ use database to store the prefix information ]\n")
   printf("\t-c | --cust   [ customer name to use ]\n")
   printf("\t-f | --file   [ DB configuration file to use ]\n")
   printf("\t-n |          [ DRY RUN, does not insert into DB ]\n")
   printf("\t   | --debug  [ Be verbose ]\n")
   printf("\t-h | --help   [ prints this help ]\n")
   os.exit(1)
end

-- Get the hash 
function gethash()
   local cksum = "sha1"
   local random = nil
   local hash = nil
   local temp = nil
   local min = 100
   local max = 1000000
   local alg = nil

   -- This is my lua code in C, using getifaddrs and getting the mac address
   -- by looking up the AF_LINK flag in OpenBSD and AF_PACKET in Linux
   for address, mac in pairs(lintf.ether()) do
      -- Get a string, with interface name, mac address
      if random then
         random = random .. address .. mac
      else
         random = address .. mac 
      end

      random = random .. math.random(min, max) .. os.time()
      random = random .. digest.hmac(cksum, os.time())
   end

   -- Make the random string a giant string with date() and a random number
   -- from max and min
   random = random .. os.date() .. math.random(min, max) .. os.time()
   random = random .. digest.hmac(cksum, random) 

   -- Get the digest and return it
   return digest.hmac(cksum, random)
end

function GetIPv6(Length, Prefix)
   -- this is what I am returning to the caller
   local retval = nil

   -- We do not want to have floating numbers, so we are flooring it
   -- What is my prefixLen?
   local prefixLen = math.floor(Length / 4)

   -- Find out how many digits we are offset from the bitline boundry
   -- If the Length aligns to bitline boundry, the howMany will be zero (
   -- Einstein hehe:) )
   local howMany = math.floor(math.fmod(Length, 16) / 4)

   -- If it is a prefix, then we subtract 2 as we have to accomodate fd at
   -- the begining, which will take 2 of them, loosely based on RFC4193
   if Prefix then
      prefixLen = prefixLen - 2
   end

   -- Get the hash of MAC + DATE + RANDOM
   local hash = gethash()

   verbose("debug: hash %s", hash)

   -- We start from 1 and then do a <= in the for loop
   local index = 1

   -- This is the initial fd, again loosely based on RFC4193
   local rfc4193 = "fd"

   -- We iterate through the string, using split equivalent --> gmatch
   for ix in string.gmatch(hash, ".") do
      -- this is the for loop that I mentioned above, start at 1 and go all
      -- the way to prefixLen (inclusive)
      if index <= prefixLen then

         -- If retval is not null then add the string to it
         if retval then
            retval = retval .. ix
         else
            -- If prefix is set and retval is not null we start with fd
            -- else we start from ix
            if Prefix then
               retval = rfc4193 .. ix
            else
               if howMany ~= 0 then
                  -- Prepend zero for prefixes which are not in bitline
                  -- boundry
                  if howMany == 1 then
                     retval = "000" .. ix
                  elseif howMany == 2 then
                     retval = "00" .. ix
                  elseif howMany == 3 then
                     retval = "0" .. ix
                  end
               else
                  retval = ix
               end
            end
         end

         -- We need to know when to add colon, so we use value, instead of
         -- using same function twice, which is not pretty
         local value = 0
         if Prefix then
            -- If prefix, it is not divisible by 4, and we need to know when
            -- to add the colon either way using the same function, so we
            -- add two and make the if loop below happy by adding colon at
            -- the correct place
            value = index + 2
         else
            -- Have the correct offset for colons, if the length of IPv6
            -- address requested does not fall under the bitline boundry
            if howMany == 1 then
               value = index + 3
            elseif howMany == 2 then
               value = index + 2
            elseif howMany == 3 then
               value = index + 1
            else
               value = index
            end
         end

         -- Make sure we are mod of 4 and the index is lesser than the 
         -- prefixLen, this will avoid having colons at the end, which is
         -- not necessary for the IPv6 node
         if ((math.fmod(value, mod) == 0) and (index < prefixLen)) then 
            retval = retval .. ":"
         end

         -- similar to python no ++ operator support
         index = index + 1
      end
   end
   
   -- if the prefix is not a multiple of 16, meaning it is not in bitline
   -- boundry, then we need to add trailing zeros, otherwise the system will
   -- intpret the IPv6 address with leading zeroes, resulting in bit math
   -- error
   if Prefix then
      if howMany == 1 then
         retval = retval .. "000"
      elseif howMany == 2 then
         retval = retval .. "00"
      elseif howMany == 3 then
         retval = retval .. "0"
      end
   end

   if Prefix then
      -- If I want to generate a /128 then I do not need a :: at the end
      -- else I do
      if Length ~= ipv6MaxLen then
         retval = retval .. "::/" .. Length
      else
         retval = retval .. "/" .. Length
      end
   end

   -- I returned, good good function
   return retval 
end

-- MySQL function, takes the dbtable and SQLQuery
function MySQL(db, sqlQuery)
   local mysql = luasql.mysql()
   local temp = "mysql"
   local cursor = nil
   local retval = false
   -- mysql port
   local sqlport = 3306
   
   local conn = mysql:connect(temp, 
      db["db"], db["user"], db["password"], db["server"], sqlport)
   
   if conn then      
      if sqlQuery then 
         cursor = assert(conn:execute(sqlQuery))
      else
         printf("no sql query given")
      end
   else
      err("cannot get MySQL handle!")
   end

   -- If my query is insert, then the cursor is a number not an
   -- iterator, without this loop we will get this error
   --     attempt to index local 'cursor' (a number value)
   local temp = tonumber(cursor)
   if not temp then
      row = cursor:fetch ({}, "a")
      while row do
         for key, value in pairs(row) do
            if value then
               retval = true
            end
         end
         row = cursor:fetch(row, "a")
      end
   
      -- lets be nice and close the cursor
      cursor:close()
   end

   conn:close()
   mysql:close()

   return retval
end

-- Simple parser function, returns a table
function parse(Filename)
   -- This is my return value
   local tbl = {}
   -- count for my line number
   local cnt = 1
   if Filename then

      -- Die gracefully, if we need to
      fh = io.open(Filename)
      if not fh then
         err("cannot open %s file", Filename)
      end

      -- iterate over the lines
      for line in io.lines(Filename) do
         -- split the line into tokens with space as delimiter
         local token = split(line, "%s+")
         -- What is my token length, this is useful when we encounter an
         -- empty line
         local tokenLen = getSize(token)
         if tokenLen > 0 then
            -- go through the token, assignments are explantory
            if ((token[1] == "set") and (token[2] == "dbase")) then
               if (token[3] == "server_IP") then
                  tbl["server"] = token[4]
               elseif (token[3] == "name") then
                  tbl["db"] = token[4]
               elseif (token[3] == "user") then
                  tbl["user"] = token[4]
               elseif (token[3] == "password") then
                  tbl["password"] = token[4]
               else
                  -- If I do not know the token at the 3 column we
                  -- error out
                  err("unknown token %s in line %d", token[3], cnt)
               end
            elseif (token[1] ~= "#")  then
               -- since this is a rudimentary parser, comment lines are
               -- lines that start with # sign and only if the # sign is at
               -- the begining of the line
               err("unknown tokens in line %d", cnt)
            end
         end

         -- my line number
         cnt = cnt + 1
      end
   end

   -- return
   return tbl
end

function dbOpt(Cust, dbCfg, IPv6Prefix)
   -- If no cust then we error out!
   local str = nil
   local sqlQuery = nil

   if not Cust then
      err("no customer name defined")
   end

   -- dbCfg
   if dbCfg then
      -- this is where db parameters are stored in a table
      local dbt = {}
      -- If the cfgFile is defined, we parse it and the db values
      if dbCfg then
         dbt = parse(dbCfg)
      end

      local tblSize = getSize(dbt)
      if tblSize == 0 then
         err("no database parameters for the db option")
      end

      -- If IPv6Prefix is defined
      if IPv6Prefix then
         str = "select id from IPv6_Prefix "
         sqlQuery = string.format("%s where prefix = \"%s\";", str, IPv6Prefix)

         printf("%s\n", sqlQuery)
         local retval = MySQL(dbt, sqlQuery)
         if retval then
            err("%s prefix already exists", IPv6Prefix)
         end

         str = "insert into IPv6_Prefix values(NULL,"
         sqlQuery = string.format("%s \"%s\", \"%s\");", str, Cust, IPv6Prefix)

         printf("%s\n", sqlQuery)
         if not dry then
            retval = MySQL(dbt, sqlQuery)
         end
      end
   end
end

function GetOpt()
   -- rudimentry getopt function, lua does not have one, can you believe it
   local prefix = false
   local db = false
   local custName = nil
   local cfgFile = nil
   local length = 0
   local minPrefixLen = 12

   -- Remember there is no concept of arrays in lua, everything is a
   -- hash(perl)/dict(python), only difference is the key will be consecutive
   -- numbers in table (meaning it is an array)
   for key, value in pairs(arg) do
      -- I dont want to parse anything on or below 0 as 
      -- -1 will always be lua and 0 will be argv[0], so I only care
      -- from upwards of zero
      if (key > 0) then
         -- What happens when the user type -h or --help
         if ((value == "-h") or (value == "--help")) then
            usage()
         elseif ((value == "-p") or (value == "--prefix")) then
            prefix = true
         elseif ((value == "-l") or (value == "--length")) then
            -- Check to see wheher we have the value for length in the
            -- next index, otherwise error out similar to getopt
            if not arg[key + 1] then
               err("%s option requires numric argument", value)
            else
               -- Check to see whether the option is a number,
               -- to number gives nil back when it is not a number
               length = tonumber(arg[key + 1])
               if (length == nil) then
                  err("%s option requires numeric value", value)
               end
               -- This is important as the for loop will go nuts if the 
               -- following is not in place. The loop will think that the
               -- value passed to the --length option is infact an option
               -- and we not have a elseif block to take care of it, the loop 
               -- will default to the else block which will error out.
               -- Once we are done parsing the value, remove the contents
               -- from the table and only error out when we do not recognize
               -- an actual option passed via command line
               -- XXX:
               -- The following should be used sparsely, as it will take a 
               -- penalty on the performance if the table has more than say 100
               -- entries. 
               --
               -- Having said that..
               -- I will worry about when I have 100 options to be passed
               -- via command line
               table.remove(arg, (key + 1))
            end
         elseif ((value == "-d") or (value == "--db")) then
            db = true
         elseif ((value == "-c") or (value == "--cust")) then
            -- make sure comment has some value passed from the CLI
            if not arg[key + 1] then
               err("%s option requires argument", value)
            else
               custName = arg[key + 1]
            end
            -- same as above
            table.remove(arg, (key + 1))
         elseif ((value == "-f") or (value == "--file")) then
            -- make sure filename has some filename passed from the CLI
            if not arg[key + 1] then
               err("%s option requires argument", value)
            else
               cfgFile = arg[key + 1]
            end
            -- same as above
            table.remove(arg, (key + 1))
         elseif (value == "-n") then
            -- Toggle dry run
            dry = true
         elseif (value == "--debug") then
            -- Toggle debug
            debug = true
         else
            -- default, if the user type some random option
            -- notify the user that the option is bogus and print usage,
            -- which is exit
            printf("%s: unknown option: %s\n", program, value)
            usage()
         end
      end
   end

   if length <= 0 then
      err("length cannot be zero or less than zero")
   elseif length > ipv6MaxLen then
      err("Do we have IPv8 yet?, IPv6 is 128 bit only!")
   end

   -- We error out, it we need a prefix shorter than minPrefixLen as it does 
   -- not make sense, the first 8 bits is already fd, 
   -- so proceeding does not make sense
   if (prefix and (length < minPrefixLen)) then
      err("length is too short to generate a prefix")
   end

   -- XXX: 
   -- This script is not smart enough to give an answer when an odd number
   -- prefix is supplied may be in second revision
   if math.fmod(length, mod) ~= 0 then
      err("length has to be in multiples of 4 not %d", length)
   end

   -- If prefix is toggled, then generate a prefix, loosely based on
   -- RFC4193 (we use sha1, instead of md5) otherwise prints the node
   -- address with the required length
   local ipv6 = GetIPv6(length, prefix)
   -- We will check whether we have ipv6 defined, otherwise we do not 
   -- execute the loop
   if ipv6 then
      local facility = "LOG_WARNING"
      if prefix then
         printf("%s: prefix generated with %d bit length: %s\n", 
            program, length, ipv6)
         logging(facility, "prefix generated with %d bit length: %s\n", 
            length, ipv6)
      else
         printf("%s: IPv6 node address with %d bit length: %s\n", 
            program, length, ipv6)
         logging(facility, "IPv6 node address with %d bit length: %s\n", 
            length, ipv6)
      end
   end

   -- if the -f or --file option is not given, then we default
   -- to the following file
   if not cfgFile then
      cfgFile = "/opt/local/etc/db.conf"
   end

   -- If -d or --db option is used
   if db then
      -- If prefix option is not selected, we do not do database insert
      if not prefix then
         printf("database will not be used for IPv6 node address generation\n")
      else
         -- dbOpt
         dbOpt(custName, cfgFile, ipv6)
      end
   end
end

-- getargv, 
-- pythoners this is similar to 
-- program = os.path.split(sys.argv[0])[1],
-- perl monks, this is nothing but:
-- push @my prog, split(/\//, $0);
-- my $program = $prog[$#prog];
program = getargv(split(arg[0], "[\\/]+"))

-- Refer down below
main = not pcall(getfenv, 4)

-- pythoners similar to len(sys.argv), perl monks this is scalar(@ARGV)
argc = getn(arg)

-- pythoners, similar to if __name__ == "__main__"
if main then
   -- If I dont have any arguments, then pring usage otherwise go to
   -- GetOpt function
   if (argc <= 1) then
      usage()
   else
      -- never returns, but can error out
      GetOpt()
   end
end
